feat: Agent Teams TUI dashboard and message delivery on exit#23
Conversation
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 2 minutes and 5 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (16)
📝 WalkthroughWalkthroughThis PR introduces a message delivery mechanism in the spawner to handle pending messages when teammates exit, updates the tools to report unread messages directed to "lead", and adds a new terminal UI package with components for displaying team state, tasks, messages, and activity logs. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (7)
packages/agent-teams-ui/src/store.ts (1)
73-75: Avoid fully silent fallback on read/parse failures.Right now malformed files are indistinguishable from “no data”, which makes triage hard. Consider logging a short warning (stderr is enough) before returning fallback.
Proposed adjustment
async function readJson<T>(filePath: string, fallback: T): Promise<T> { if (!existsSync(filePath)) return fallback; try { const raw = await readFile(filePath, "utf-8"); return JSON.parse(raw) as T; - } catch { + } catch (error) { + process.stderr.write( + `[agent-teams-ui] failed to read/parse ${filePath}: ${String(error)}\n` + ); return fallback; } } async function readLog(filePath: string): Promise<string[]> { if (!existsSync(filePath)) return []; try { const raw = await readFile(filePath, "utf-8"); return raw.split("\n").filter(Boolean); - } catch { + } catch (error) { + process.stderr.write( + `[agent-teams-ui] failed to read ${filePath}: ${String(error)}\n` + ); return []; } }Also applies to: 83-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/agent-teams-ui/src/store.ts` around lines 73 - 75, The catch blocks that simply "return fallback" swallow parse/read errors and make failures indistinguishable from missing data; update those catch handlers (the ones returning the fallback variable) to log a short warning to stderr (e.g., console.warn or process.stderr.write) that includes a brief context message and the caught error before returning fallback so malformed files are visible during triage.packages/agent-teams-ui/src/components/ChatView.tsx (2)
83-84:g/Gkey bindings appear inverted from vim conventions.In vim,
g(orgg) goes to the top andGgoes to the bottom. Here,gsetsscrollOffsetto max (showing oldest entries at top), andGsets it to 0 (showing newest at bottom). If the intent is to show "top of log" vs "bottom of log", consider whether users expect vim-style navigation.If intentional (newest = bottom = G), a brief comment would help clarify.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/agent-teams-ui/src/components/ChatView.tsx` around lines 83 - 84, The keybindings in ChatView.tsx are inverted versus vim: change the handlers so pressing "g" moves to the top (setScrollOffset(0)) and pressing "G" moves to the bottom (setScrollOffset(Math.max(0, filtered.length - viewHeight))); update the two lines that currently call setScrollOffset for input === "g" and input === "G" accordingly (or, if the current behavior was intentional, replace with an inline comment next to the setScrollOffset calls explaining the non-vim convention to avoid confusion).
97-120: Mutation during render works but is non-idiomatic.The
lastSourcevariable is mutated during themap()call (line 120). While this works because rendering is synchronous, it can confuse readers expecting functional-style React. Consider pre-computing a grouped structure if readability becomes a concern, but this is fine for now.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/agent-teams-ui/src/components/ChatView.tsx` around lines 97 - 120, The render mutates lastSource during visible.map (lastSource variable) which is non-idiomatic; remove lastSource and compute showName functionally by comparing the current entry with the previous one (e.g., use the map index: showName = i === 0 || visible[i - 1].source !== entry.source) inside the visible.map callback, keeping getAgentColor(entry.source, agentColors) usage unchanged (or alternatively precompute a grouped/partitioned structure before rendering if you prefer grouping).packages/agent-teams-ui/src/App.tsx (3)
71-78: File watcher cleanup may not complete before unmount.
watcher.close()returns aPromise<void>in chokidar 4.x, but the cleanup function returnsvoid. While React doesn't await cleanup functions, the watcher may not fully close before the component unmounts in fast remount scenarios.Consider handling this explicitly if cleanup timing matters:
♻️ Optional: explicit async cleanup pattern
return () => { watcher.close(); }; + // Note: watcher.close() is async in chokidar 4.x + // For strict cleanup, store watcher in ref and handle in separate effect🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/agent-teams-ui/src/App.tsx` around lines 71 - 78, The cleanup currently calls watcher.close() synchronously in the useEffect return but chokidar's watcher.close() returns a Promise<void>, so the watcher may not finish closing before unmount; update the useEffect cleanup to call watcher.close() and handle the returned Promise (e.g., call watcher.close().catch(() => {}) or create an async IIFE to await watcher.close()) to ensure errors are swallowed/logged and the close promise is handled; locate the useEffect, watcher variable and the watch(...) call in App.tsx (references: useEffect, watcher, watch, refresh, workspacePath, join) and modify the cleanup to handle the Promise returned by watcher.close().
91-98: Arrow key navigation restricted on certain tabs.The arrow key tab switching is disabled when on
tasksorchattabs (lines 91, 95). This is presumably to avoid conflicts with in-tab navigation (j/k scrolling). Consider adding a comment to clarify this intentional UX decision for future maintainers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/agent-teams-ui/src/App.tsx` around lines 91 - 98, The arrow-key handling in the keyboard shortcut block (checking key.tab, key.rightArrow, key.leftArrow, using TABS, tab, and switchTab) deliberately skips switching when tab === "tasks" or tab === "chat" to avoid conflicting with in-tab j/k scrolling; add a concise inline comment above both conditional checks explaining this UX decision so future maintainers understand the intentional restriction and don't remove it accidentally.
49-52: Task status comparison relies on index-based matching.Line 50 compares
prev.tasks[i]?.status !== t.statususing array indices. If tasks are reordered between refreshes, this would detect false-positive status changes. For activity indicators this is acceptable, but for precise change detection, consider comparing by task ID.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/agent-teams-ui/src/App.tsx` around lines 49 - 52, The current comparison in next.tasks.some((t, i) => prev.tasks[i]?.status !== t.status) uses index-based matching and can false-positive when tasks are reordered; change it to match tasks by a stable identifier (e.g., task.id) before comparing statuses: build a lookup for prev.tasks keyed by id (or use find by id) and then set activity.team = true if any next task's status differs from the prev task with the same id. Update the logic around next.tasks.some, prev.tasks, and activity.team to use the id-based comparison.packages/agent-teams-lead/src/spawner.ts (1)
136-141: Fire-and-forget async call in synchronous event handler.
deliverPendingMessagesis an async method but is called withoutawaitin theexithandler. This means any errors would be unhandled promise rejections (though the method has its own try/catch). Additionally, the process cleanup on lines 139-140 proceeds immediately without waiting for message delivery to complete.If message delivery should complete before cleanup, consider awaiting. Otherwise, the current pattern is acceptable since errors are already caught internally.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/agent-teams-lead/src/spawner.ts` around lines 136 - 141, The exit handler on proc (proc.on("exit", ...)) calls the async method deliverPendingMessages(teammate.id, teammate.name) without awaiting it, so cleanup (this.processes.delete and this.cleanupAgentConfig) runs immediately; decide to await delivery if it must finish before cleanup by turning the handler into an async flow (or chain the promise) and await deliverPendingMessages before calling this.processes.delete(teammate.id) and this.cleanupAgentConfig(configPath); otherwise, explicitly document the fire-and-forget intent and keep current ordering. Ensure you reference deliverPendingMessages, this.processes.delete, and this.cleanupAgentConfig in the change so the message delivery completes (or is intentionally non-blocking) prior to cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/agent-teams-lead/src/tools.ts`:
- Around line 213-216: The pending_messages returned by waitForTeam are never
acknowledged, so leadMessages from this.store.getMessages({ to: "lead",
unread_by: "lead" }) will be returned repeatedly; update waitForTeam to
acknowledge those messages after including them in pending_messages by calling
the store's message-acknowledgement API (e.g., markMessageRead /
markMessagesRead / updateMessage to clear unread_by for each message id) on the
leadMessages array (or their ids) so subsequent calls won't return the same
messages; ensure the acknowledgement runs only after messages are successfully
included in the response and handle any errors from the store acknowledgement
path.
In `@packages/agent-teams-ui/package.json`:
- Around line 7-9: The package's CLI binary "agent-teams-ui" points to
"./dist/index.js" but the TypeScript compile step removes the shebang from
src/index.tsx, breaking global installs; update the package.json "build" script
to preserve the shebang after tsc (for example, add a post-build step to prepend
"#!/usr/bin/env node" to dist/index.js) or switch the build pipeline to a
bundler (e.g., esbuild) configured to preserve shebangs so that the generated
dist/index.js retains the interpreter line.
In `@packages/agent-teams-ui/src/components/Header.tsx`:
- Around line 40-41: The progress bar width calculation can produce negative
values when width < 40; update the logic around barW and filled to clamp
negative sizes to zero: compute an available width as Math.max(0, width - 40)
and then set barW = Math.min(20, available) so barW is never negative, then
recompute filled = Math.round((barW * pct) / 100) and ensure filled is bounded
between 0 and barW before using symbols.line.repeat(filled); change references
in this block (barW, filled, width, pct, symbols.line.repeat) accordingly.
In `@packages/agent-teams-ui/src/components/MessagesView.tsx`:
- Around line 73-76: The sender grouping isn't reset at date boundaries because
showName is only comparing msg.from_name to lastFrom; update the showName
calculation in MessagesView (the variable showName that uses msg.from_name and
lastFrom) to also consider showDate so that the first message of a new date
always shows the sender label (e.g., set showName to true when msg.from_name !==
lastFrom OR showDate), then continue to update lastFrom as before.
In `@packages/agent-teams-ui/src/components/TasksView.tsx`:
- Around line 89-90: The column width calculation can produce a negative value
(causing symbols.line.repeat to throw) when the terminal is very narrow; update
the computation of colWidth (derived from totalWidth, COLUMNS and
stdout?.columns) to clamp to a non-negative value (e.g., use Math.max(0,
Math.floor(...)) or ensure totalWidth >= required minimum) and also guard any
use of symbols.line.repeat by passing Math.max(0, colWidth) so repeats never
receive a negative count; change references to totalWidth, colWidth, COLUMNS and
the repeat call to use this clamped value.
- Around line 96-101: The allTasks aggregation in TasksView.tsx is built in the
wrong order (in_progress -> pending -> blocked -> completed) which mismatches
the rendered columns (pending -> in_progress -> blocked -> completed); update
the allTasks construction (and the other similar allTasks occurrence) to concat
filters in the UI order: pending, in_progress, blocked, completed so
highlight/traversal aligns with the grid rendering (reference the allTasks
variable in the TasksView component).
- Around line 111-113: The down-arrow handler can set selectedIdx negative when
allTasks is empty because allTasks.length - 1 is -1; update the handler in the
block that checks key.downArrow || input === "j" to guard or clamp using the
non-negative max index (e.g., compute maxIndex = Math.max(0, allTasks.length -
1) or early-return when allTasks.length === 0) and then call setSelectedIdx(i =>
Math.min(maxIndex, i + 1)); reference the variables and functions:
key.downArrow, input === "j", allTasks, maxIndex (new), and setSelectedIdx to
locate where to change.
In `@packages/agent-teams-ui/src/components/TeamView.tsx`:
- Around line 62-65: The "waiting for tasks" message is shown too broadly;
change the JSX condition that renders the Box (currently using !current &&
doneCount === 0) to also require totalAssigned === 0 so only teammates with no
assigned tasks show "waiting for tasks"; locate the conditional around the
Box/Text rendering in TeamView.tsx and update it to check current, doneCount,
and totalAssigned (use the totalAssigned variable used elsewhere in this
component) before rendering that message.
In `@packages/agent-teams-ui/src/index.tsx`:
- Line 11: The catch on waitUntilExit() currently swallows errors by calling
process.exit(0); change it to surface failures by logging the error and exiting
with a non-zero code or rethrowing: update the promise rejection handler for
waitUntilExit() to capture the error (e.g., err) and call process.exit(1) after
logging (or rethrow) instead of exiting with 0 so startup/runtime failures are
visible to CI; locate the waitUntilExit() call and the process.exit usage in
index.tsx to make this change.
---
Nitpick comments:
In `@packages/agent-teams-lead/src/spawner.ts`:
- Around line 136-141: The exit handler on proc (proc.on("exit", ...)) calls the
async method deliverPendingMessages(teammate.id, teammate.name) without awaiting
it, so cleanup (this.processes.delete and this.cleanupAgentConfig) runs
immediately; decide to await delivery if it must finish before cleanup by
turning the handler into an async flow (or chain the promise) and await
deliverPendingMessages before calling this.processes.delete(teammate.id) and
this.cleanupAgentConfig(configPath); otherwise, explicitly document the
fire-and-forget intent and keep current ordering. Ensure you reference
deliverPendingMessages, this.processes.delete, and this.cleanupAgentConfig in
the change so the message delivery completes (or is intentionally non-blocking)
prior to cleanup.
In `@packages/agent-teams-ui/src/App.tsx`:
- Around line 71-78: The cleanup currently calls watcher.close() synchronously
in the useEffect return but chokidar's watcher.close() returns a Promise<void>,
so the watcher may not finish closing before unmount; update the useEffect
cleanup to call watcher.close() and handle the returned Promise (e.g., call
watcher.close().catch(() => {}) or create an async IIFE to await
watcher.close()) to ensure errors are swallowed/logged and the close promise is
handled; locate the useEffect, watcher variable and the watch(...) call in
App.tsx (references: useEffect, watcher, watch, refresh, workspacePath, join)
and modify the cleanup to handle the Promise returned by watcher.close().
- Around line 91-98: The arrow-key handling in the keyboard shortcut block
(checking key.tab, key.rightArrow, key.leftArrow, using TABS, tab, and
switchTab) deliberately skips switching when tab === "tasks" or tab === "chat"
to avoid conflicting with in-tab j/k scrolling; add a concise inline comment
above both conditional checks explaining this UX decision so future maintainers
understand the intentional restriction and don't remove it accidentally.
- Around line 49-52: The current comparison in next.tasks.some((t, i) =>
prev.tasks[i]?.status !== t.status) uses index-based matching and can
false-positive when tasks are reordered; change it to match tasks by a stable
identifier (e.g., task.id) before comparing statuses: build a lookup for
prev.tasks keyed by id (or use find by id) and then set activity.team = true if
any next task's status differs from the prev task with the same id. Update the
logic around next.tasks.some, prev.tasks, and activity.team to use the id-based
comparison.
In `@packages/agent-teams-ui/src/components/ChatView.tsx`:
- Around line 83-84: The keybindings in ChatView.tsx are inverted versus vim:
change the handlers so pressing "g" moves to the top (setScrollOffset(0)) and
pressing "G" moves to the bottom (setScrollOffset(Math.max(0, filtered.length -
viewHeight))); update the two lines that currently call setScrollOffset for
input === "g" and input === "G" accordingly (or, if the current behavior was
intentional, replace with an inline comment next to the setScrollOffset calls
explaining the non-vim convention to avoid confusion).
- Around line 97-120: The render mutates lastSource during visible.map
(lastSource variable) which is non-idiomatic; remove lastSource and compute
showName functionally by comparing the current entry with the previous one
(e.g., use the map index: showName = i === 0 || visible[i - 1].source !==
entry.source) inside the visible.map callback, keeping
getAgentColor(entry.source, agentColors) usage unchanged (or alternatively
precompute a grouped/partitioned structure before rendering if you prefer
grouping).
In `@packages/agent-teams-ui/src/store.ts`:
- Around line 73-75: The catch blocks that simply "return fallback" swallow
parse/read errors and make failures indistinguishable from missing data; update
those catch handlers (the ones returning the fallback variable) to log a short
warning to stderr (e.g., console.warn or process.stderr.write) that includes a
brief context message and the caught error before returning fallback so
malformed files are visible during triage.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 44228c72-692b-467a-88f9-42bfc3392c48
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (14)
packages/agent-teams-lead/src/spawner.tspackages/agent-teams-lead/src/tools.tspackages/agent-teams-ui/package.jsonpackages/agent-teams-ui/scripts/seed.tspackages/agent-teams-ui/src/App.tsxpackages/agent-teams-ui/src/components/ChatView.tsxpackages/agent-teams-ui/src/components/Header.tsxpackages/agent-teams-ui/src/components/MessagesView.tsxpackages/agent-teams-ui/src/components/TasksView.tsxpackages/agent-teams-ui/src/components/TeamView.tsxpackages/agent-teams-ui/src/index.tsxpackages/agent-teams-ui/src/store.tspackages/agent-teams-ui/src/theme.tspackages/agent-teams-ui/tsconfig.json
| const barW = Math.min(20, width - 40); | ||
| const filled = Math.round((barW * pct) / 100); |
There was a problem hiding this comment.
Progress bar width could be negative on very narrow terminals.
If width < 40, then barW becomes negative (e.g., Math.min(20, 30 - 40) = -10). This would cause filled to be negative and symbols.line.repeat(filled) to throw or produce empty output.
🛡️ Proposed fix
- const barW = Math.min(20, width - 40);
+ const barW = Math.max(0, Math.min(20, width - 40));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const barW = Math.min(20, width - 40); | |
| const filled = Math.round((barW * pct) / 100); | |
| const barW = Math.max(0, Math.min(20, width - 40)); | |
| const filled = Math.round((barW * pct) / 100); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/agent-teams-ui/src/components/Header.tsx` around lines 40 - 41, The
progress bar width calculation can produce negative values when width < 40;
update the logic around barW and filled to clamp negative sizes to zero: compute
an available width as Math.max(0, width - 40) and then set barW = Math.min(20,
available) so barW is never negative, then recompute filled = Math.round((barW *
pct) / 100) and ensure filled is bounded between 0 and barW before using
symbols.line.repeat(filled); change references in this block (barW, filled,
width, pct, symbols.line.repeat) accordingly.
e6151b9 to
a4f5867
Compare
- Add TUI dashboard (Ink) with 4 tabs: Team, Board, Messages, Chat - Board: kanban-style columns with task selection (j/k) and detail view (Enter) - Messages: WhatsApp-style group chat with date separators and sender grouping - Chat: agent stdout as conversation with filter by agent (f) and scroll (j/k) - Header: live timer, progress bar, new activity indicators per tab - Deliver pending messages to teammates on CLI exit (spawner) - Include lead messages in wait_for_team response - Add seed script for demo data
a4f5867 to
eaea139
Compare
- Ack lead messages after returning them in wait_for_team - Add shebang preservation to build step - Clamp progress bar and column widths for narrow terminals - Reset sender grouping at date boundaries in messages - Fix allTasks order to match rendered column order - Guard selectedIdx against empty task list - Fix 'waiting for tasks' condition to check totalAssigned - Exit with code 1 on TUI errors - Fix g/G keybindings to match vim convention
What
TUI dashboard para visualizar agent teams em tempo real e entrega automática de mensagens pendentes quando um teammate termina a execução.
Changes
agent-teams-lead (spawner)
exit), lêmessages.jsone loga todas as mensagens pendentes noteam.logwait_for_teamagora retornapending_messagescom mensagens direcionadas ao lead que ainda não foram lidas (tanto no sucesso quanto no timeout)agent-teams-ui (novo package)
TUI interativa com Ink (React no terminal) com 4 tabs:
[done/total], task atual com duraçãoj/k, detail view comEntermostrando description, criteria, summary, files, notesf, scroll comj/k,g/Gtop/bottomHeader com:
[done/total tasks X%]*)Auto-refresh via chokidar watching
.agent-teams/.How to test
cd packages/agent-teams-ui npx tsx scripts/seed.ts npx tsx src/index.tsx __demo__Summary by CodeRabbit